/**
 * @see https://cn.mobx.js.org/#mobx-4-vs-mobx-5
 * MobX 4 vs MobX 5
 * MobX 4 和 MobX 5 的不同之处在于后者使用了 ES6 的 proxy 来追踪属性。因此，MobX 5 只能运行在支持 proxy 的浏览器上，而 MobX 4可以运行在任何支持 ES5 的环境中。
 * MobX 4 的重要局限性:
 * Observable 数组并非真正的数组，所以它们无法通过 Array.isArray() 的检查。最常见的处理方法是在传递给第三方库之前，你经常需要先对其进行 .slice() 操作，从而得到一个浅拷贝的真正数组。
 * 向一个已存在的 observable 对象中添加属性不会被自动捕获。要么使用 observable 映射来替代，要么使用[工具函数] 中方法来对想要动态添加属性的对象进行读/写/迭代。
 * @see https://cn.mobx.js.org/refguide/object-api.html [工具函数]
 */

import * as mobx4 from './mobx@4';
// import * as mobx5 from './mobx@5'

const _mobx = (typeof window['Proxy'] === 'undefined' || typeof window['Symbol'] === 'undefined') ? mobx4 : mobx4;
const autorun = _mobx.autorun;
const reaction = _mobx.reaction;
const when = _mobx.when;
const action = _mobx.action;
const comparer = _mobx.comparer;
const runInAction = _mobx.runInAction;
const configure = _mobx.configure;
const computed = function (...args): any {
    const [arg1, arg2, arg3] = args;
    return _mobx.computed(arg1, arg2, arg3);
};
const observable = function (...args): any {
    const [arg1, arg2, arg3] = args;
    return _mobx.observable(arg1, arg2, arg3);
};

/**
 * 配置 - 必须通过action修改值
 * enforceActions 也被称为“严格模式”。
 * 在严格模式下，不允许在 action 外更改任何状态。 可接收的值:
 * "never" (默认): 可以在任意地方修改状态
 * "observed": 在某处观察到的所有状态都需要通过动作进行更改。在正式应用中推荐此严格模式。
 * "always": 状态始终需要通过动作来更新(实际上还包括创建)。
 */
configure({ enforceActions: 'observed' });

/************************************************MobX工具函数************************************************/
/**
 * 现在有一个统一的工具 API 可以操控 observable 映射、对象和数组。
 * 这些 API 都是响应式的，这意味着如果使用 set 进行添加，使用 values 或 keys 进行迭代，即便是新属性的声明都可以被 MobX 检测到。
 */
/**
 * 将集合中的所有值作为数组返回
 * @example
 * values(thing)
 */
const values = _mobx.values;
/**
 * 将集合中的所有键作为数组返回
 * @example
 * keys(thing)
 */
const keys = _mobx.keys;
/**
 * 返回集合中的所有项的键值对数组
 * @example
 * entries(thing)
 */
const entries = _mobx.entries;

/**
 * [由于MobX4的局限性可能需要用到以下方法]
 */
/**
 * 使用提供的键值对来更新给定的集合
 * @see https://cn.mobx.js.org/refguide/object-api.html
 * @example 
 * set(thing, key, value)
 * set(thing, { key: value })
 */
const set = _mobx.set;
/**
 * 返回指定键下的子项
 * @see https://cn.mobx.js.org/refguide/object-api.html
 * @example
 * get(thing, key)
 */
const get = _mobx.get;
/**
 * 从集合中移除指定的项。用于数组拼接
 * @see https://cn.mobx.js.org/refguide/object-api.html
 * @example
 * remove(thing, key)
 */
const remove = _mobx.remove;
/**
 * 如果集合中存在指定的 observable 属性就返回 true
 * @see https://cn.mobx.js.org/refguide/object-api.html
 * @example
 * has(thing, key)
 */
const has = _mobx.has;

/**
 * @example
 * import { get, set, observable, values } from "mobx@4"
 * 
 * const twitterUrls = observable.object({
 *     "John": "twitter.com/johnny"
 * })
 *
 * autorun(() => {
 *     console.log(get(twitterUrls, "Sara")) // get 可以追踪尚未存在的属性
 * })
 *
 * autorun(() => {
 *     console.log("All urls: " + values(twitterUrls).join(", "))
 * })
 *
 * set(twitterUrls, { "Sara" : "twitter.com/horsejs"})
 */
const mobx = {
    autorun,
    reaction,
    when,
    action,
    comparer,
    runInAction,
    configure,
    computed,
    observable,
    values,
    keys,
    entries,
    set,
    get,
    remove,
    has
};
export default mobx;

/**********************一些输出**********************/
function log(str, ...args) {
    console.log(`[mobx] [log] ${str}`, ...args);
}

function warn(str, ...args) {
    console.warn(`[mobx] [warn] ${str}`, ...args);
}

function error(str, ...args) {
    console.error(`[mobx] [error] ${str}`, ...args);
}

/**********************数据绑定MOBX**********************/
/**
 * 文档：https://cn.mobx.js.org/
 * CDN：https://unpkg.com/mobx/lib/mobx.umd.js
 */
/**
 * @see https://cn.mobx.js.org/
 * @see https://www.jianshu.com/p/d1e8fbc94c78
 * observable   [修饰] [观察数据变化的方法]
 * computed     [修饰] [观察计算属性的变化，可以设置参数] @see https://cn.mobx.js.org/refguide/computed-decorator.html
 * action       [修饰] [改变数值，修饰改变数值的方法]
 * action.bound [修饰] [改变数值，action中的异步方法可以调用被action.bound修饰的方法进行修改数值]
 * runInAction  [方法] [改变数值，用在action中的异步方法里或await之后来进行修改数值操作]
 * autorun      [修饰] [监听数值变化，一上来就执行，数据改变时执行，可重复执行]
 * reaction     [修饰] [监听数值变化，条件函数一上来就执行，条件函数的返回值发生变化时，效果函数将被执行，可重复执行，可手动停止]
 * when         [修饰] [监听数值变化，条件函数一上来就执行，条件函数返回值为true时，效果函数将被执行，只执行一次]
 * @example
 * // 修改数据一定要放在runInAction里面执行、也可以在被action修饰过的函数里面修改数据
 * runInAction(() => { data.a = 1; })
 * autorun(()=>{ () => { console.log(data.a); })
 * // reaction.dispose()可以手动停止监听，dispose方法也会被作为reaction的返回值返回
 * var dispose = reaction(()=>{ return data.a > 1}, (a, reaction) => { if(a > 1) reaction.dispose(); })
 * // when的条件里面写入想要监听的数据，可以是多个
 * when(()=>{ return data.a + data.b > 1}, () => { console.log(123); })
 * 
 */
// const { autorun, reaction, when, configure, action, computed, observable, comparer, runInAction } = mobx;

const mobxDisposerKey = '_$mobx_ccclass_disposer$';
const mobxExecuteKey = '_$mobx_ccclass_execute$';

function mobxModifyCcclassFun(prototype, bindFunName: string, unbindFunName: string, executeName: string, funName: string, condition?: Function) {
    const bindArrKey = mobxExecuteKey + bindFunName;
    const unbindMapKey = mobxDisposerKey + unbindFunName;

    const oldBindFunName = mobxExecuteKey + bindFunName + '_old';
    const oldUnbindFunName = mobxExecuteKey + unbindFunName + '_old';

    // 待绑定数组
    if (!prototype[bindArrKey]) Object.defineProperty(prototype, bindArrKey, { value: [] });
    prototype[bindArrKey].push([executeName, funName, condition]);

    // 修改原始方法
    if (!prototype[bindFunName] || !prototype[bindFunName].mobx_ccclass_bind) {
        // 缓存原方法
        if (prototype[bindFunName]) {
            Object.defineProperty(prototype, oldBindFunName, { value: prototype[bindFunName] });
        } else if (CC_DEBUG) {
            log(`[${executeName}] ${cc.js.getClassName(prototype)}.${bindFunName}方法已自动添加`);
        }
        // 定义新方法
        prototype[bindFunName] = function (...args) {
            this[oldBindFunName] && this[oldBindFunName](...args);

            // 待解绑Map
            if (!this[unbindMapKey]) Object.defineProperty(this, unbindMapKey, { value: Object.create(null) });
            this[bindArrKey].forEach((arg) => {
                // 已经存在先停止旧的
                if (this[unbindMapKey][arg[1]]) {
                    this[unbindMapKey][arg[1]]();
                }
                // 再生成新的
                if (arg[2]) {
                    this[unbindMapKey][arg[1]] = mobx[arg[0]](arg[2].bind(this, this), this[arg[1]].bind(this));
                } else {
                    this[unbindMapKey][arg[1]] = mobx[arg[0]](this[arg[1]].bind(this));
                }
            });
        };
        prototype[bindFunName].mobx_ccclass_bind = true;
    }

    if (!prototype[unbindFunName] || !prototype[unbindFunName].mobx_ccclass_unbind) {
        if (prototype[unbindFunName]) {
            Object.defineProperty(prototype, oldUnbindFunName, { value: prototype[unbindFunName] });
        } else if (CC_DEBUG) {
            log(`[${executeName}] ${cc.js.getClassName(prototype)}.${unbindFunName}方法已自动添加`);
        }
        prototype[unbindFunName] = function (...args) {
            for (const key in this[unbindMapKey]) {
                this[unbindMapKey][key]();
            }
            cc.js.clear(this[unbindMapKey]);
            this[oldUnbindFunName] && this[oldUnbindFunName](...args);
        };
        prototype[unbindFunName].mobx_ccclass_unbind = true;
    }
}

function _ccautorun(...args) {
    const [prototype, funName, descriptor, bindFunName = 'onLoad', unbindFunName = 'onDestroy'] = args;

    // 某些key不允许被修饰
    if (funName === 'constructor') {
        return error('[autorun] 禁止修饰constructor');
    }

    // 修饰的必须是个方法
    if (typeof descriptor.value !== 'function') {
        return error('[autorun] 只允许修饰方法, 禁止修饰变量或get/set');
    }

    mobxModifyCcclassFun(prototype, bindFunName, unbindFunName, 'autorun', funName);
}
/**
 * 对mobx中autorun的封装
 * 以确保在autorun将在指定的函数被调用时才执行或销毁(执行顺序早于这个函数原本的内容)
 * 这个函数一般是cc的声明周期函数，默认是onLoad绑定，onDestory解绑，可以自定义
 * @example
 * @ccautorun('start', 'onDisable')
 * test(){ console.log(data.num) }
 * autorun将在start被调用时执行，在onDisable调用时销毁
 */
export function ccautorun(bind?: string, unbind?: string): Function;
export function ccautorun(prototype: Object, key: any, descriptor?: any): any;
export function ccautorun(...args): any {
    if (typeof args[2] === 'undefined' || typeof args[2].configurable === 'undefined') {
        // 传入自定义参数
        return function (prototype, key, descriptor) {
            _ccautorun(prototype, key, descriptor, args[0], args[1]);
        };
    } else {
        _ccautorun(...args);
    }
}

function _ccreaction(...args) {
    const [prototype, funName, descriptor, condition, bindFunName = 'onLoad', unbindFunName = 'onDestroy'] = args;

    // 某些key不允许被修饰
    if (funName === 'constructor') {
        return error('[autorun] 禁止修饰constructor');
    }

    // 修饰的必须是个方法
    if (typeof descriptor.value !== 'function') {
        return error('[autorun] 只允许修饰方法, 禁止修饰变量或get/set');
    }

    mobxModifyCcclassFun(prototype, bindFunName, unbindFunName, 'reaction', funName, condition);
}
/**
 * 对mobx中reaction的封装
 * 以确保在reaction将在指定的函数被调用时才执行或销毁(执行顺序早于这个函数原本的内容)
 * 这个函数一般是cc的声明周期函数，默认是onLoad绑定，onDestory解绑，可以自定义
 * @example
 * @ccreaction(function(){ retrun data.num > 10 }, 'start', 'onDisable')
 * test(){ console.log(data.num) }
 * reaction将在start被调用时执行，在onDisable调用时销毁
 */
export function ccreaction(condition: Function, bind?: string, unbind?: string): Function;
// export function ccreaction(prototype: Object, key: any, descriptor?: any): any;
export function ccreaction(...args) {
    // 传入自定义参数
    if (typeof args[2] === 'undefined' || typeof args[2].configurable === 'undefined') {
        return function (prototype, key, descriptor) {
            _ccreaction(prototype, key, descriptor, args[0], args[1], args[2]);
        };
    } else {
        _ccreaction(...args);
    }
}

function _ccwhen(...args) {
    const [prototype, funName, descriptor, condition, bindFunName = 'onLoad', unbindFunName = 'onDestroy'] = args;

    // 某些key不允许被修饰
    if (funName === 'constructor') {
        return error('[autorun] 禁止修饰constructor');
    }

    // 修饰的必须是个方法
    if (typeof descriptor.value !== 'function') {
        return error('[autorun] 只允许修饰方法, 禁止修饰变量或get/set');
    }

    mobxModifyCcclassFun(prototype, bindFunName, unbindFunName, 'when', funName, condition);
}
/**
 * 对mobx中when的封装
 * 以确保在when将在指定的函数被调用时才执行或销毁(执行顺序早于这个函数原本的内容)
 * 这个函数一般是cc的声明周期函数，默认是onLoad绑定，onDestory解绑，可以自定义
 * @example
 * @ccwhen(function(){ retrun data.num > 10 }, 'start', 'onDisable')
 * test(){ console.log(data.num) }
 * when将在start被调用时执行，在onDisable调用时销毁
 */
export function ccwhen(condition: Function, bind?: string, unbind?: string): Function;
// export function ccwhen(prototype: Object, key: any, descriptor?: any): any;
export function ccwhen(...args) {
    // 传入自定义参数
    if (typeof args[2] === 'undefined' || typeof args[2].configurable === 'undefined') {
        return function (prototype, key, descriptor) {
            _ccwhen(prototype, key, descriptor, args[0], args[1], args[2]);
        };
    } else {
        _ccwhen(...args);
    }
}

export function ccaction(...args) {
    return mobx.action(...args);
}

export function ccobservable(...args) {
    return mobx.observable(...args);
}

export function cccomputed(...args) {
    return mobx.computed(...args);
}